Intention.js offers a light-weight and clear way to dynamically restructure HTML in a responsive manner.

Easily increase layout options and flexibility, reduce development time and lessen dependence on media-query-driven stylesheet overrides.

Download the latest bundle
intention.js + context.js
Build your own contexts
intention.js v0.9.9
<body>
   <header>
      <img intent
         in-standard-src="med.png"
         in-mobile-src="small.png" />
   </header>
   <nav intent
      in-abovethefold-after="header"
      in-belowthefold-prepend="body"
      in-belowthefold-class="sticky">
   </nav>
   <section intent
      in-daytime-class="light"
      in-nighttime-class="dark">
         Hello, World!
   </section>

   <script>
      intent.on('nighttime', function() {
         $('section').text("Goodnight, World!");
      });
   </script>
</body>

      
Outputs on desktops:
<body> <header> <img src="med.png" /> </header>
<nav> </nav>
<section class="dark"> Goodnight, World! </section> </body>
Outputs on mobile:
<body> <nav class="sticky"> </nav>
<header> <img src="small.png" /> </header>
<section class="light"> Hello, World! </section> </body>

  
This input
<style> #smallCode.mobile #output{ color:orange } #smallCode.smalltablet #output{ color:blue } #smallCode.tablet #output{ color:green } </style> <section id="smallCode" intent in-width:> <pre id="output" intent in-width: in-mobile-after="#input" in-smalltablet-after="#input" in-tablet-after="#input"> //output </pre> <pre id="input"> //input </pre> </section>

Try it!

What It's all About

Intention.js is a lightweight tool for responsive design developed at Dow Jones that manipulates the DOM via HTML attributes. The methods for manipulation are placed with the elements themselves, so flexible layouts don't have to be so abstract and messy.

What should an element's classes be on mobile vs tablet? Where should advertising markup be placed when viewed on a desktop browser? Does the page require an alternate slideshow widget on touch-enabled devices? These are all scenarios that Intention.js can handle, altering the page based onusers' devices. Context.js creates a set of common page contexts for width thresholds, touch devices, high-res displays and a fallback.

And you can easily add your own contexts on top of these, or create all your own custom threshold group.

Let's start with the basics, though.

Thresholds, Axes, & Contexts

Intention.js works by determining what are called “threshold groups”. These threshold groups define the context in which a user is viewing your site. Threshold groups work by measuring an axis, like the browser’s width or the device’s pixel density (for high resolution screens).

A lot of these contexts are predictable, and some common patterns are included in a handy implementation of intention.js called context.js

Axis
   - context_name, value

width (a window of at least x pixels wide)
   - mobile, 0
   - tablet, 510
   - standard, 840
orientation (degree of window.rotation)
   - portrait, 0
   - landscape, 90
touch (window.ontouchstart boolean)
   - touch, true
highres (devicePixelRatio > 1)
   - highres, true

Manipulations via HTML Attributes

Intention.js has three basic manipulations: attribution manipulations, class manipulations, and placement manipulations. With these three, you can change the value of any attribute, add or remove an element’s class, and adjust the position of an element within the structure of the document.

Usage

Basic Syntax

Giving Intention.js instructions is as easy as flagging the element as "intentional" and giving it an intentional attribute.


Intentional attributes can be specific to:
  • A change in contexts within an axis
  • A specific context passing true
  • A specific context within a specific axis passing true

For the purposes of documentation, in- will be used instead of the proper HTML-valid data-in-.

<elm attr="val"
   intent
   in-axisID:
   in-axisID:contextName-attr="val"
   in-contextName-attr="val">
</elm>
<div intent in-width:></div>

<img intent
   in-orientation:landscape-src="wide.png" />

<nav intent in-touch-prepend="#content" />

Dependencies

Intention.js requires jQuery and Underscore.js to work. You can download and link to them manually, or you can include them via require.

<script
   data-main="assets/js/context"
   src="assets/js/require/require.js">
</script>
   

Compatibility

Intention.js is tested to work on all modern browsers, including Internet Explorer back to IE8! Woo-hoo!

Note: jQuery 2.x dropped support for IE8, so obviously using it in conjunction with Intention will not work in IE8. If you don't care about that, rest assured there is nothing Intention needs in jQuery 1.x that isn't available in jQuery 2.x.

Intention Markup

<header>
   <img src="logo.png" intent
      in-highres-src="retina.png" />
</header>

<nav intent
   in-mobile-prepend="#content"
   in-tablet-prepend="#content"
   in-standard-after="header"
   in-touch-class="swipeDrawer">

   <a id="about" intent
      in-mobile-href="about.html"
      in-tablet-href="about.html"
      in-standard-href="#about">About</a>
   <a id="projects" href="/faq"
      intent
      in-mobile-before="#about">FAQ</a>

</nav>

<div id="content" intent
   in-width: in-orientation:>
   This is all so easy!
</div>
    
On an iPhone 5
<header>
   <img src="retina.png" />
</header>

<div id="content"
   class="mobile portrait">

   <nav class="swipeDrawer">
      <a href="/faq">
         FAQ
      </a>
      <a href="about.html">
         About
      </a>
   </nav>

   This is all so easy!

</div>

      
On Regular Tablets
<header>
   <img src="logo.png" />
</header>

<div id="content"
   class="tablet portrait">

   <nav
      class="swipeDrawer">
      <a href="about.html">
         About
      </a>
      <a href="/faq">
         FAQ
      </a>
   </nav>

   This is all so easy!

</div>
      
On Desktops
<header>
   <img src="logo.png" />
</header>

<nav>
   <a href="about.html">
      About
   </a>
   <a href="/faq">
      FAQ
   </a>
</nav>

<div id="content"
   class="standard">

   This is all so easy!

</div>

      
<div id="timeExample"
   intent in-time:>
</div>
      
var time = intent.responsive({
   ID: 'time',
   contexts: [
      {name:'night',min:20},
      {name:'evening',min:17},
      {name:'sunset',min:16},
      {name:'day',min:9},
      {name:'morning',min:0}
   ],
   matcher: function(test, ctx) {
      return test >= ctx.min;
   },
   measure: function(arg) {
      var time = new Date();
      return time.getHours();
   }
});
time.respond(); //check the current context
   
time.respond('');

Intention.js responds to a bunch of device contexts, but it’s open to any index. You can create custom contexts based on anything you can measure, and restructure your code in response!

It’s not just changing a page based on the browser’s width: it’s noticing touch-capabilities, portrait/landscape orientation, high resolution contexts. Intention.js can be taught to restructure pages based on scroll depth, pageviews, time of day—basically anything!

Manipulations

Class Manipulations

The simplest intentional attribute is a class manipulation. This manipulation adds the current context as a class to the element. Adding in-axis_name: (note the trailing colon) to a flagged element is enough to get it working.

<img intent in-orientation: src="a.jpg" />
      

↓ ↓ ↓

<!--In portrait orientation-->
<img class="portrait" src="a.jpg" />

<!--In landscape orientation-->
<img class="landscape" src="a.jpg" />
      

Intention does not touch attributes that are assigned outside of in- commands — so

<div class="foo" intent in-width: />
      

will keep its class foo regardless of what horizontal_axis context is passed.

Attribute Manipulation

Intention.js can also manipulate an element's attributes with more specificity than can be achieved via class manipulations. To start, set a base (default) attribute in case no contexts are met, then specify context-specific attribute values.

<img intent
   in-base-src="reg_img.png"
   in-highres-src="big_img.png" />
      

↓ ↓ ↓

<!--On regular devices-->
<img src="reg_img.png" />

<!--On retina displays-->
<img src="big_img.png" />
      

Attribute manipulation can used for more specific class manipulations, too.

<section intent
   in-mobile-class="narrow"
   in-tablet-class="medium"
   in-standard-class="wide" />
      

Placement Manipulation

Intention.js can rearrange elements within a page layout based on the context.

Suppose we want to demote the status of the navigation when the user is on smaller devices. The following specification on the navigation might do what we need:

<header>
   <nav intent
      in-mobile-prepend="footer"
      in-tablet-append="section"
      in-standard-append="header">
   </nav>
   <section> ... </section>
   <footer> ... </footer>
</header>
      

When the device is 320px wide or less, the navigation will sit at the top of the footer. When the device is between 321px and 768px wide, it will appear right below the section. Obviously, on larger displays (wider than 769px) the navigation will be at the end of the header.

Move functions

Intention.js provides four basic functions for rearranging elements. They include:

- prepend
- append
- before
- after
      

These function just like jQuery DOM manipulations.

intent in-ctxName-moveFx="selector"
      

Selectors can be general element selections like above (...prepend="footer"), or specific (...prepend="#intro").

Play with me
B
C
var mini = intent.responsive({
   ID: 'mini',
   contexts: [
      {name:"large",min:300},
      {name:"avg",min:75}
   ],
   matcher: function(test, context) {
      return test >= context.min;
   },
   measure:function(arg) {
      return $("#resizable").width();
   }
});
mini.respond();
//'resize' is a jQueryUI event
$('#resizable')
   .on('resize', mini.respond);
      
<div id="resizable">
   <div class="orange" intent in-mini:>
      Play with me
      <div id="orange1"> </div>
      <div id="orange2"> </div>
   </div>
   <div class="blue" intent in-mini:
      in-avg-before="#orange2"
      in-large-after=".orange"> </div>
   <div class="green" intent in-mini:
      in-avg-after=".orange"
      in-large-append=".blue">
      <div id="green1">
         <a intent
            in-avg-href="#average"
            in-large-href="#large"> A </a>
      </div>
      <div id="green2"> B </div>
      <div id="green3"> C </div>
   </div>
</div>
   

Custom Axes & Contexts

One of intention.js's most exciting features is its scalability: you can use it to respond to almost anything! Any type of data that is quantifiable can be used to manipulate the page. Of course context.js comes with the most common patterns of responsive design, but let's take a look at how to create our own.

Markup

Each axis is made up of four basic properties: the axis ID (optional), the context group, the matcher function, and the measure function,. As we know, an axis is a measurable object or set of information. This axis is optionally given an ID so it can be used in specific manipulations. A measure function finds the current measurement of that axis, and the matcher function finds where that measurement lies in a set of thresholds and breakpoints called contexts. Contexts are defined in an ordered array and help dictate when the DOM should be manipulated.

var axis = intent.responsive({
   ID: 'axisID',
   contexts: [
      {name:'ctx1', val:'3'},
      {name:'ctx2', val:'2'},
      {name:'ctx3', val:'1'}
   ],
   matcher: function(measure, context){
      return measure >= context.val;
   },
   measure: function(){
      return someMeasurement;
   }
});

First things first

If you are completely abandoning context.js, you must be sure to first create an Intention object. This is required for any axis creation or response. Context.js does this right out of the box, so if you are only extending the included axes, you need not create a new Intention object. Be sure to check out the next section Initialization to see what else context.js takes care of—things you'll have to do when starting from scratch.

var intent = new Intention();

// axis creation

// be sure the Intention object
// is saved to a global variable
// if you want to use it
// across plugins
window.intent = intent;

Contexts

To work with whatever data we measure, we need to set up contexts that will act as thresholds. The contexts property is an ordered array of objects. Each context object represents a range of data. If a measurement falls in that range of data, the corresponding context passes true. Then that context is set as the current context.

When a measurement is being matched against the contexts, the function will iterate through the array in order, so the breakpoints must be listed in an increasing or decreasing order. If a context passes true, the array is immediately exited.

Each context object must have a name property. This is used to identify exactly what context is true. Be sure the context's name is a string!

var axis = intent.responsive({
// ...
    contexts: [
       {name:'ctx3', val:'20'},
       {name:'ctx2', val:'10'},
       {name:'ctx1', val:'0'}
    ],
//
// Here we use a descending value order
// and our matcher function will be
// written accordingly
// 
// ...
});

Measure Function

The measure function's task is simply to find data that will later be matched against the contexts. It can be a series of complex operations, but as long as it returns a value, it's doing its job. In most cases, this function is just a return that passes off a value to the matcher function.

var axis = intent.responsive({
// ...
measure: function(){
   return someMeasurement
}
});

Matcher Function

The matcher function uses the measure function's returned value and the context array as parameters. It tests the measurement against each context's value property. When a measurement fits in a context's data range, the context passes true. This is done with a comparative statement. For example, if a measurement does not exceed the maximum value of a context, then the context will pass true. If it does, then the matcher function will see if exceeds the next context's maximum value (which will be a higher value).

The comparative statement must agree with the order of the contexts. If context values are listed in descending order, the matcher function must test if the measurement is greater than or equal to the context minimum value. If it is, then we know it definitely is greater than all of the other contexts' minimum values.

var axis = intent.responsive({
// ...
matcher: function(measure, context){

   // for contexts arranged in
   // greatest-to-least order
   // (exits array when the measure
   //  is greater than the minimum)
   return measure >= context.min;
   
   // for contexts arrange in
   // least-to-greatest order
   // (exits array when the measure
   //  is less than the maximum)
   return measure <= context.max;
   
   // the default matcher function
   // looks for an exact match
   return measure === context.val;
   
   // be sure to compare the measurement
   // to the context's value, not just
   // the context object
   
},
// ...
});

Axis IDs

An axis ID property allows for context-aware restructuring. Although this property is optional, it is used in HTML attributes to command DOM manipulations. An axis ID lets you create manipulations for any change in contexts in a specified axis, or for any specific context within a specific axis.

Without an assigned ID, the axis is randomly given a hash as an ID that changes on every page load—not very helpful for making specific changes to the layout.

var axis = intent.responsive({
   ID:'axisID', 
   contexts: [
      {name:'ctx3', val:'20'},
      {name:'ctx2', val:'10'},
      {name:'ctx1', val:'0'}
   ],
   matcher: function(measure, ctx.min){
      return measure >= ctx.min;
   },
   measure: function(){
      return someMeasurement;
   }
});
<div intent in-axisID:></div>
<img intent in-axisID:ctx3-src="..." />

Putting it all together

Responding

Every intent.responsive axis returns some useful properties, of which respond is probably the most important. This property contains a function that sets off the measure and matcher functions. Calling axis.respond() updates the current context within an axis.

Note that the intent.responsive({...})'s variable name is used to for responding, not the axis ID. Keep variable scope in mind! You can always use intent.axes.axisID.respond() if you are out of the axis variable's scope.

var axis = intent.responsive({
// ...
});

axis.respond(); // respond once

$(window).on('event', axis.respond);
// respond on each instance of 'event'

If your axis needs only respond once, you can make it do so right after you create the axis with a trailing respond command. The touch axis is such an axis: Intention needs only test touch capabilities at page load because they are unlikely to change mid-session.

var axis = intent.responsive({
// ...
}).respond();

// note that you cannot have the
// axis respond again after this
// axisID.respond(); won't work

Finding the Current Context

Another useful property intent.responsive returns is current. Calling this property will return the name of the most recent context passed as a string.

// assuming your Intention() object
// is saved as "intent"

intent.axes.axisID.current

Intialization

If you're not using context.js, there's a few things you need to do to get Intention working. Context.js does this for you, but you may find yourself using intention.js for something other than standard responses.

Finding Elements

Once all the contexts have been written, Intention must search the DOM for elements that have been flagged "intentional". It will find each element and create a record of it and its manipulations in an array of objects intent.elms. Every specified manipulation will be saved in this array as instructions, so when an axis or context is passed the manipulation is more immediate.

To perform this search, first construct all of your axes and threshold groups, then run this function at doc ready.

$(function(){
  intent.elements(document);
});
         

Should you need to inject intentional HTML after the page load, you can always add elements to this registry using intent.add().

For demonstration purposes, here's an example of an object saved in the element array.

[...
↓ Object
   → elm: elementObject
   ↓ spec: manipulationsObject
      __move__:"#movementTarget" 
      __placement__:"moveMethod"
      ↓ axisName: Object
         ↓ contextName: Object
            class:"className"
            attribute:"value"
            
↓ Object
   → elm: section#output
   ↓ spec: Object
      __move__:"#input"
      __placement__:"after"
      ↓ width: Object
         standard: Object
            class:"standard"
         tablet: Object
            class:"tablet"
         mobile: Object
            class="mobile"
...]

Extending Intention For Other Plugins

Context.js returns the Intentional object right out of the box, so you can use it across plugins and files; but if you are just using intention.js and your own custom axes, you might want to also extend the Intentional object yourself.

Allowing the Intentional object to be used in plugins and other files is as simple as saving it to a global variable. At the end of your document, simply create a variable for the window scope that matches the variable name for the Intention object you created in the very beginning.

var intent = new Intention();

//...
//all your stuff
//...

window.intent = intent;
(function(){
   var intent = new Intention(),
   
   axisName = intent.responsive({
      ID: 'axisID',
      contexts: [
         {name:'context1', min:1},
         {name:'context2', min:0}
      ],
      matcher: function(measure, context){
         return measure >= context.min;
      },
      measure: function() {
         return someMeasurement;
      }
   });
       
   $(window).on('event', axisName.respond);
   
   $(function() {
      intent.elements(document);
   });
   
   //save it to a global variable
   window.intent = intent;
});

Intent Events

It's possible (and very easy) to set up event listeners for context changes. Intention.js supports event binding to scenarios such as:

  1. When a specific context passes true
  2. When a specific context in a specific axis passes true
  3. When a specific axis passes any context

The syntax generally follows jQuery's syntax for event binding. Preface the .on() event handler with the namespace intent.

The event itself is made up of two optional parts: an axis ID and a context name. The axis ID refers to the axis' ID property and should always be followed by a : . Using the axis ID on its own will fire every time a context in that axis is passed. Specifying a context name will narrow the event to fire only when that context within that axis is passed.

Alternatively, an event handler can be created for contexts without a specified axis. Simply excluding the axis ID component (and its trailing :) will make a more general event listener for the specified context name. In this scenario, be careful of naming conflicts. If any two contexts share the same name, both will fire this same event.

Below is a list of reference IDs for the axes supplied in context.js

axis: ID
   - contexts

horizontal_axis: width
   - standard
   - tablet
   - mobile
orientation_axis: orientation
   - portrait
   - landscape
touch: touch
highres: highres

1. When a specific context passed true.

intent.on('contextName', function() {
// ...
});

intent.on('mobile', function() {
// ...
});

2. When a specific context in an specific axis passes true

intent.on('axisID:contextName', function() {
// ...
});

intent.on('width:mobile', function() {
// ...
});

3. When a specific axis passes any context

intent.on('axisName:', function() {
// where axisName is the axis' ID property
// ...
});

intent.on('width:', function() {
// ...
});

Note: currently, Intention does not support multiple event types per event handler. You will have to chain events instead.

Event Handlers for Intention's First Response

There are cases when you need to listen for Intention's first response on page load—maybe to insert specific content or run certain functions. There are obviously a number of ways to do this, but here are two simple methods.

One-time Page Load Responses

On certain occasions you may only care about the first context passed when the page first loads. After that first load, you may not want the event handler to keep firing. In this scenario, we can use jQuery's Deferred Objects.

var init = function(contexts, callback){
   var dfds = [],
   _.each(contexts, function(ctx){
      var dfd = $.Deferred();
      
      dfds.push(dfd);
      
      if(intent.is(ctx)) {
         dfd.resolve();
      } else {
         intent.on(ctx, dfd.resolve);
      }
   });
   $.when.apply(this, dfds).done(callback);
};

init(['mobile'], function() {
console.log('mobile ctx is first passed');
});
init(['standard', 'tablet'], function() {
console.log('both standard and tablet have been passed');
});

The above function init() accepts two parameters: an array of context names to be passed and a callback function that is triggered when the array is satisfied. With supplied each context, init() creates a deferred object and adds it to a master array.

If the user is already in that context when the function is called, the deferred object will be resolved. If the user is not already in that specific context, init will create an event handler to wait for the context to pass true, when it will resolve the deferred object. When all of the deferred objects in the master array have been resolved, the callback function will fire once and only once.

Repeating Event Handlers

If you do not care about the first response on page load exclusively but want to create an event handler that still affects the first response, the easiest way is to edit context.js. We want to have all event handlers be written before the affected axes respond. Otherwise, the first manipulations will have already occurred before the event handler is even created.

var intent = new Intention();

// Event handlers can be created
// before the axis is even written
intent.on('standard', function() {
//   ... 
});

var horizontal_axis = intent.responsive({
   ID: 'width',
   contexts: [
      {name:'standard', min:840}, 
      {name:'tablet', min:768},
      {name:'mobile', min:0}
   ],
   matcher: function(measure, context) {
      return measure >= context.min;
   },
   measure: function() {
      return $(window).width();
   }
});

// Just as long as they are made
// before the axis responds
// and after the intent object

intent.on('width:', function() {
//   ...
});

horizontal_axis.respond();

$(window)
.on('resize', horizontal_axis.respond);

window.intent = intent;

$(function() {
   intent.elements(document);
});

About

Intention.js was developed by Joe Kendall at the Dow Jones Consumer Technology Group, with contributions from Erin Sparling, Tyler Paige, Adrian Lafond, and Mike Stamm. Major contributions to documentation and examples were provided by Tyler Paige, Camila Mercado and Paul Pangrazzi.

License

MIT license for everything

Copyright (c) 2012 The Wall Street Journal,
http://wsj.com/

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.